// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/binance/api_order/native_private_client.h"

#include <list>
#include <memory>
#include <sstream>
#include <string>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/binance/symbology/product.h"
#include "coin2/exchange/binance_base/api_util/rate_tracker.h"

using coin2::exchange::binance::symbology::BinanceProduct;
using AsioHttpContextList = std::list<std::shared_ptr<AsioHttpContext>>;

#define CHECK_API_LIMIT(action_type)                                                      \
  if (!HasEnoughRateLimit(ts, action_type)) {                                             \
    LOG_EVERY_N(WARNING, 50) << "Hit rate limit or api temporary disabled, " << __func__; \
    api_block_info_[__func__]++;                                                          \
    return {};                                                                            \
  }

namespace {
using ActionType = binance::api_util::BinanceRateTracker::ActionType;
}

auto BinanceNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  auto params = RestParams();

  BinanceProduct product =
      BinanceProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  params.set_symbol(order_spec.product);
  auto formatter = product_holder->product_info().order_formatter();
  if (order_spec.order_type != OrderType::MARKET_ORDER) {
    params.set_price(formatter.FormatPrice(order_spec.price));
  }
  params.set_quantity(formatter.FormatQty(order_spec.qty));
  params.set_new_client_order_id(std::to_string(proc_order_id));
  params.set_recv_window(5000);

  if (order_spec.post_only) {
    params.set_type("LIMIT_MAKER");
  } else if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_type("MARKET");
  } else {
    params.set_type("LIMIT");
    if (order_spec.duration == coin::proto::OrderDuration::FOK_ORDER) {
      params.set_time_in_force("FOK");
    } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
      params.set_time_in_force("IOC");
    } else {
      params.set_time_in_force("GTC");
    }
  }

  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("BUY");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("SELL");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }

  return params;
}

auto BinanceNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  auto params = RestParams();
  const std::string& product_str = order_context.order_spec().product;
  params.set_symbol(product_str);
  if (order_context.has_exchange_order_id()) {
    params.set_order_id(order_context.exchange_order_id().substr(product_str.length()));
  } else {
    params.set_orig_client_order_id(std::to_string(order_context.proc_order_id()));
  }

  params.set_recv_window(5000);
  return params;
}

BinanceNativePrivateClient::BinanceNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config_override) {
  mea_ = mea;
  using coin2::exchange::binance::api_util::BinanceRateTracker;
  NativeClientConfig config;
  config.set_rest_host("api.binance.com");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[BinanceNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<BinanceRestAuth, NativeClientConfig>(config);
  rate_tracker_ = std::make_unique<BinanceRateTracker>(mea_, config);
  start_time_ = GetCurrentTimestamp();
}

BaseRestAuth* BinanceNativePrivateClient::GetAuth() const {
  return auths_.GetRandomTryNotUseFirst();
}

void BinanceNativePrivateClient::SetBindIp(const std::string& ip) {
  static_cast<BinanceRateTracker*>(rate_tracker_.get())->SetBindIp(ip);
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::QueryAccountBalance(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  UrlParamsBuilder builder;
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/api/v3/account", &builder));
  SetBindIp(context->bind_ip);

  // 20210908(daniel): query balance may be blocked due to rate limit consumed by IP,
  // which blocks system be ready
  if (!is_first_query_account_balance_) {
    CHECK_API_LIMIT(ActionType::QUERY);
  }
  is_first_query_account_balance_ = false;

  Push(10, ts, ActionType::QUERY);
  api_req_info_[__func__]++;
  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::QueryFlexibleProductPosition(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  UrlParamsBuilder builder;
  builder.PutGet("size", 100);
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(GetAuth()->CreateRequestByGetWithSignature(
      "/sapi/v1/simple-earn/flexible/position",
      &builder));
  SetBindIp(context->bind_ip);

  CHECK_API_LIMIT(ActionType::QUERY);

  Push(150, ts, ActionType::QUERY);
  api_req_info_[__func__]++;
  context->query_type = BinanceOrderSubTopicId::REST_FLEXIBLE_PRODUCT_POSITION;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::QueryLockedProductPosition(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  UrlParamsBuilder builder;
  builder.PutGet("size", 100);
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/sapi/v1/simple-earn/locked/position", &builder));
  SetBindIp(context->bind_ip);

  CHECK_API_LIMIT(ActionType::QUERY);

  Push(150, ts, ActionType::QUERY);
  api_req_info_[__func__]++;
  context->query_type = BinanceOrderSubTopicId::REST_LOCKED_PRODUCT_POSITION;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::QueryMarginCrossedAccountBalance(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  UrlParamsBuilder builder;
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/sapi/v1/margin/account", &builder));
  SetBindIp(context->bind_ip);

  if (!is_first_query_account_balance_) {
    CHECK_API_LIMIT(ActionType::QUERY);
  }
  is_first_query_account_balance_ = false;

  Push(10, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_CROSS_BALANCE;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::QueryMarginIsolatedAccountBalance(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  UrlParamsBuilder builder;
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/sapi/v1/margin/isolated/account", &builder));
  SetBindIp(context->bind_ip);

  if (!is_first_query_account_balance_) {
    CHECK_API_LIMIT(ActionType::QUERY);
  }
  is_first_query_account_balance_ = false;

  Push(10, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_NON_CROSS_BALANCE;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::QueryOpenOrders(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  std::string symbol = "ALL";
  int weight = 40;  // by sub-account
  UrlParamsBuilder builder;
  if (params.has_symbol() && !params.symbol().empty()) {
    builder.PutGet("symbol", params.symbol());
    symbol = params.symbol();
    weight = 3;  // by symbol
  }
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/api/v3/openOrders", &builder));
  SetBindIp(context->bind_ip);

  // as IP based rate limit is tight for binance spot, this is for making sure the open orders are
  // queried.
  if (query_open_orders_ts_map_.count(symbol) &&
      ts < query_open_orders_ts_map_[symbol] + 120'000'000'000LL /* 2mins */) {
    CHECK_API_LIMIT(ActionType::QUERY);
  }
  query_open_orders_ts_map_[symbol] = ts;

  Push(weight, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::QueryAllOrders(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  builder.PutGet("limit", params.limit());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/api/v3/allOrders", &builder));
  SetBindIp(context->bind_ip);

  CHECK_API_LIMIT(ActionType::QUERY);

  Push(10, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ALL_ORDERS;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::QueryTrades(const RestParams& params) {
  DLOG_EVERY_N(INFO, 50) << DebugString();
  auto ts = GetCurrentTimestamp();

  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  builder.PutGet("limit", params.limit());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/api/v3/myTrades", &builder));
  SetBindIp(context->bind_ip);

  CHECK_API_LIMIT(ActionType::QUERY);

  Push(10, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::PlaceOrder(const RestParams& params) {
  auto ts = GetCurrentTimestamp();

  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("side", params.side());
  builder.PutGet("type", params.type());
  builder.PutGet("timeInForce", params.time_in_force());
  builder.PutGet("price", params.price());
  builder.PutGet("quantity", params.quantity());
  builder.PutGet("quantityOrderQty", params.quantity_order_qty());
  builder.PutGet("newClientOrderId", params.new_client_order_id());
  builder.PutGet("stopPrice", params.stop_price());    // not used
  builder.PutGet("icebergQty", params.iceberg_qty());  // not used
  builder.PutGet("newOrderRespType", params.new_order_resp_type());
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetFirstAuth()->CreateRequestByPostWithSignature("/api/v3/order", &builder));
  SetBindIp(context->bind_ip);
  CHECK_API_LIMIT(ActionType::PLACE);
  Push(1, ts, ActionType::PLACE);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::CancelOrder(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("orderId", params.order_id());
  builder.PutGet("origClientOrderId", params.orig_client_order_id());
  builder.PutGet("newClientOrderId", params.new_client_order_id());
  builder.PutGet("recvWindow", params.recv_window());
  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetFirstAuth()->CreateRequestByDeleteWithSignature("/api/v3/order", &builder));
  SetBindIp(context->bind_ip);

  CHECK_API_LIMIT(ActionType::CANCEL);

  Push(1, ts, ActionType::CANCEL);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::GenListenKey(bool is_init) {
  if (is_init && !listen_key_.empty()) {
    return nullptr;
  }
  auto ts = GetCurrentTimestamp();
  // CHECK_API_LIMIT(ActionType::QUERY);

  UrlParamsBuilder builder;
  auto context = std::shared_ptr<AsioHttpContext>(
      GetFirstAuth()->CreateRequestByPostWithSignature("/api/v3/userDataStream", &builder));
  SetBindIp(context->bind_ip);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::LISTEN_KEY;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::ExtendListenKey() {
  auto ts = GetCurrentTimestamp();
  // CHECK_API_LIMIT(ActionType::QUERY);

  UrlParamsBuilder builder;
  auto context = std::shared_ptr<AsioHttpContext>(
      GetFirstAuth()->CreateRequestByPutWithSignature("/api/v3/userDataStream", &builder));
  SetBindIp(context->bind_ip);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::LISTEN_KEY;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::QueryDeposit(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  UrlParamsBuilder builder;
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("startTime", params.start_time());
    builder.PutGet("endTime", params.end_time());
  }
  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/sapi/v1/capital/deposit/hisrec", &builder));
  SetBindIp(context->bind_ip);
  CHECK_API_LIMIT(ActionType::QUERY);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = BinanceOrderSubTopicId::REST_DEPOSIT;
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::QueryWithdrawal(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  UrlParamsBuilder builder;
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("startTime", params.start_time());
    builder.PutGet("endTime", params.end_time());
  }
  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/sapi/v1/capital/withdraw/history", &builder));
  SetBindIp(context->bind_ip);
  CHECK_API_LIMIT(ActionType::QUERY);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = BinanceOrderSubTopicId::REST_WITHDRAWAL;
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::DeleteListenKey() {
  auto ts = GetCurrentTimestamp();
  // CHECK_API_LIMIT(ActionType::QUERY);

  UrlParamsBuilder builder;
  auto context = std::shared_ptr<AsioHttpContext>(
      GetFirstAuth()->CreateRequestByDeleteWithSignature("/api/v3/userDataStream", &builder));
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  SetBindIp(context->bind_ip);
  context->query_type = OrderSubTopicId::LISTEN_KEY;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelOrder(params);
}

void BinanceNativePrivateClient::Prepare() {
  using coin2::exchange::binance::api_util::BinanceRateTracker;
  auto* tracker = dynamic_cast<BinanceRateTracker*>(this->rate_tracker_.get());
  mb_->Subscribe(
      "binance/req_rate_limit",
      std::bind(
          &BinanceRateTracker::UpdateReqRateInfo,
          tracker,
          std::placeholders::_1,
          std::placeholders::_2));
  mb_->Subscribe("binance/listen_key", [this](const std::string& channel, void* data) {
    listen_key_ = *static_cast<std::string*>(data);

    LOG(INFO) << "BinanceNativePrivateClient set listen key: " << channel << ", " << listen_key_;
  });
}

std::string BinanceNativePrivateClient::DebugString() const {
  double delta_t = (GetCurrentTimestamp() - start_time_) / 1.0e9;

  std::stringstream ss;
  ss << "BinanceNativePrivateClient::DebugString\n";
  ss << "api info\n";
  for (auto& [name, value] : api_req_info_) {
    int32_t blocked = 0;
    if (api_block_info_.count(name) > 0) {
      blocked = api_block_info_.at(name);
    }
    int32_t total_req = value + blocked;
    ss << name << " (total:" << total_req << ", succeeded: " << value << ", blocked: " << blocked
       << ", req_rate: " << value / delta_t << ", block_rate: " << blocked / delta_t << ")\n";
  }
  return ss.str();
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

// https://binance-docs.github.io/apidocs/spot/cn/#universal-transfer-for-master-account
// noted!!! Only the primary account is supported
std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::QueryTransfer(
    const RestParams& params) {
  std::string to_email = params.to_email();
  std::string from_email = params.from_email();
  if (to_email.size() > 0 && from_email.size() > 0) {
    return nullptr;
  }

  UrlParamsBuilder builder;
  auto ts = GetCurrentTimestamp();
  if (to_email.size() > 0) {
    builder.PutGet("toEmail", to_email);
  } else {
    builder.PutGet("fromEmail", from_email);
  }

  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("startTime", params.start_time());
    builder.PutGet("endTime", params.end_time());
  }

  builder.PutGet("timestamp", GetCurrMilliseconds());
  auto context = std::shared_ptr<AsioHttpContext>(GetAuth()->CreateRequestByGetWithSignature(
      "/sapi/v1/sub-account/universalTransfer",
      &builder));
  SetBindIp(context->bind_ip);
  CHECK_API_LIMIT(ActionType::QUERY);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = to_email.size() > 0 ? BinanceOrderSubTopicId::WITHDRAW_SUBACCOUNT
                                            : BinanceOrderSubTopicId::DEPOSIT_SUBACCOUNT;
  return context;
}

std::string BinanceNativePrivateClient::GetFirstKeyEmail() {
  auto auth = auths_.GetFirst();
  BinanceRestAuth* binance_auth = dynamic_cast<BinanceRestAuth*>(auth);
  return binance_auth->GetKey().email();
}

std::shared_ptr<AsioHttpContext> BinanceNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      params.set_recv_window(5000);
      return QueryAccountBalance(params);
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      return QueryOpenOrders(params);
    }
    default: {
      return nullptr;
    }
  }
}

AsioHttpContextList BinanceNativePrivateClient::SubmitRestQueryList(const RestQueryProto& query) {
  AsioHttpContextList list;
  RestParams params;
  auto fill_func = [&params](int64_t start_time, int64_t end_time) {
    params.set_start_time(start_time / 1000'000);
    params.set_end_time(end_time / 1000'000);
  };

  std::string master_email = GetFirstKeyEmail();  // eg: prestoinvt@gmail.com
  switch (query.type()) {
    case RestQueryProto::QUERY_TRANSFERS: {
      fill_func(query.start_time(), query.end_time());

      params.set_to_email(master_email);
      params.set_from_email("");
      list.emplace_back(QueryTransfer(params));
      params.set_to_email("");
      params.set_from_email(master_email);
      list.emplace_back(QueryTransfer(params));
      break;
    }
    case RestQueryProto::QUERY_WITHDRAW: {
      fill_func(query.start_time(), query.end_time());
      list.emplace_back(QueryWithdrawal(params));
      break;
    }

    case RestQueryProto::QUERY_DEPOSIT: {
      fill_func(query.start_time(), query.end_time());
      list.emplace_back(QueryDeposit(params));
      break;
    }

    default: {
      return list;
    }
  }

  return list;
}
